/*
 * ﻿Copyright (C) 2012-2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjtuil.testutil.junit;

import java.lang.reflect.UndeclaredThrowableException;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.newmainsoftech.aspectjutil.adviseutil.Util;

/**
 * Helper aspect for JUnit test.  
 * 
 * <h4>Usage example</h4>
 * <ul>
 * <li>To get test name<br />
 * <ul>
 * <li>Background:<br />
 * In JUnit 3.x, the name of current test was easily obtained with JUnit's <code>getName</code> method:
 * <blockquote><code>String testName = this.getName();</code></blockquote>
 * With JUnit 4, you need to use <code>&#64;Rule</code> or <code>&#64;Interceptor</code> annotation with 
 * <code>TestName</code> class object.<br />
 * <br />
 * </li>
 * <li>By just adding this JUnitAssistAspect class (aspectjutil-testutil.jar) to the project's aspect-path 
 * (and, of course, AspectJ's runtime library, SLF4J's API jar and binding jar files in runtime classpath, 
 * additionally to either compiling with AJC for CTW or aop.xml for LTW), no matter the version of JUnit 
 * you're using, the name of the current test can be simply obtained by:
 * 	<blockquote><code>
 * 	String testName = JUnitAssistAspect.getTestName();
 * 	</code></blockquote>
 * For further info, see <code>{@link #getTestName()}</code> method.<br />
 * <br />
 * </li>
 *  <li>Limitation<br />
 * 		If your test method spawning other thread, and call <code>{@link #getTestName()}</code> method from 
 * 		spawned thread, then it will return null as test name instead of actual test name.
 *  </li>
 * </ul>
 * </li>
 * <li>To log entrance and exit of test<br />
 * By just adding this JUnitAssistAspect class (aspectjutil-testutil.jar) to the project's aspect-path, 
 * entrance and exit of test will be logged like:
 * <blockquote>
 * <code>
 * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><br />
 * STARTS TEST: test_SysPropExt_addText<br />
 * &nbsp;&nbsp;&nbsp;&nbsp;...<br />
 * ENDS TEST: test_SysPropExt_addText<br />
 * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
 * </code>
 * </blockquote>
 * </li>
 * </ul>
 * 
 * @author <a href="mailto:artymt@gmail.com">Arata Y.</a>
 */
@Aspect
public class JUnitAssistAspect {
	private static ThreadLocal<MethodSignature> testNameThreadLocal 
	= new ThreadLocal<MethodSignature>();
		protected static ThreadLocal<MethodSignature> getTestNameThreadLocal() {
			return testNameThreadLocal;
		}
		/**
		 * Get name of running test. 
		 * This works as long as being called from main thread running each JUnit test even multiple 
		 * JUnit tests running concurrently. 
		 * But this does not work when called from child worker thread spawned during a test. To support 
		 * the call from child worker thread spawned during a test, you may make your test class implemented 
		 * {@link JUnitAssistContract} interface and implement custom logic for 
		 * {@link JUnitAssistContract#getRunningTestName()} method.
		 * @return name of running test, or null when called from child thread spawned during test.
		 */
		public static String getTestName() {
			String testName = null;
				MethodSignature methodSignature = testNameThreadLocal.get();
				if ( methodSignature == null) {
				// Avoid memory leakage for either case of having called not during test by mistake 
				// or having called from thread spawned during test 
					testNameThreadLocal.remove();
				}
				else {
					testName = testNameThreadLocal.get().getName();
					if ( testName == null) {
					// Avoid memory leakage for either case of having called not during test by mistake 
					// or having called from thread spawned during test 
						testNameThreadLocal.remove();
					}
				}
			return testName;
		}
	
	/** Dummy method for {@link Pointcut} what cross-cut execution of test method. */
	@Pointcut( value="execution( @org.junit.Test * *.*()) || execution( * junit.framework.TestCase+.test*())")
	public static void pointcutOnExecutionOfJUnitTestMethod() {}
	/** Dummy method for {@link Pointcut} what cross-cut below execution of test method. */
	@Pointcut( value="cflowbelow( pointcutOnExecutionOfJUnitTestMethod())")
	public static void pointcutOnSubsequentialExecutionOfJUnitTestMethod() {}
	/** 
	 * Dummy method for {@link Pointcut} what cross-cut top most execution of test method.
	 * This <code>Pointcut</code> is advised by 
	 * {@link #beforeAdvisedTopMostExecutionOfJUnitTestMethod(JoinPoint)} and 
	 * {@link #afterAdvisedTopMostExecutionOfJUnitTestMethod(JoinPoint)}.
	 */
	@Pointcut( value="pointcutOnExecutionOfJUnitTestMethod() " 
						+ "&& !pointcutOnSubsequentialExecutionOfJUnitTestMethod() " 
						+ "&& !execution( @org.junit.Ignore * *.*()) " 
						+ "&& !cflow( adviceexecution())")
	public static void pointcutOnTopMostExecutionOfJUnitTestMethod() {}
		/**
		 * {@link Before} advise method to preserve test method name and to log entrance of test method.
		 * There shouldn't be necessity of calling this method intentionally. 
		 * 
		 * @param joinPoint
		 * @throws Throwable
		 * @see JUnitAssistAspect#pointcutOnTopMostExecutionOfJUnitTestMethod()
		 */
		@Before( value="pointcutOnTopMostExecutionOfJUnitTestMethod()")
		public void beforeAdvisedTopMostExecutionOfJUnitTestMethod( JoinPoint joinPoint) throws Throwable {
			MethodSignature methodSignature = (MethodSignature)(joinPoint.getSignature());
			JUnitAssistAspect.getTestNameThreadLocal().set( methodSignature);
			
			Object target = joinPoint.getTarget();
			Class<?> targetClass = target.getClass();
				if ( target instanceof JUnitAssistContract) {
					JUnitAssistContract jUnitAssist = (JUnitAssistContract)target;
					try {
						jUnitAssist.setRunningTestName( methodSignature.getName());
					}
					catch( Throwable throwable) {
						if ( Util.isExceptionChecked( methodSignature, throwable)) {
							throw throwable;
						}
						else {
							throw new UndeclaredThrowableException( 
									throwable,
									String.format(
											"Occured at beforeAdvisedTopMostExecutionOfJUnit4TestMethod " 
											+ "advise method of %1$s aspect",
											JUnitAssistAspect.class.toString()
											)
									);
						}
					}
				}
			
			
			// Log entrance to test method
			Logger logger = LoggerFactory.getLogger( target.getClass());
			if ( logger.isInfoEnabled()) {
				logger.info( 
						String.format(
								"%n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>%n" 
								+ "STARTS TEST: %1$s",
								methodSignature.getName()
								)
						);
			}
		}
		/**
		 * {@link After} advise method to log exit of test method.
		 * There shouldn't be necessity of calling this method intentionally. 
		 * 
		 * @param joinPoint
		 * @see JUnitAssistAspect#pointcutOnTopMostExecutionOfJUnitTestMethod()
		 */
		@After( value="pointcutOnTopMostExecutionOfJUnitTestMethod()")
		public void afterAdvisedTopMostExecutionOfJUnitTestMethod( JoinPoint joinPoint) {
			// Log exit from test method
			Logger logger = LoggerFactory.getLogger( joinPoint.getTarget().getClass());
			if ( logger.isInfoEnabled()) {
				logger.info( 
						String.format(
								"%nENDS TEST: %1$s%n" 
								+ "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
								JUnitAssistAspect.getTestName()
								)
						);
			}
			
			JUnitAssistAspect.getTestNameThreadLocal().remove();
		}
}
